import { EventEmitter } from 'events';
import { AsyncTaskWorker, TaskFunction } from './worker';

export interface AsyncTaskRunnerOptions {
    nworkers: number;
    prefix?: string;
}

export class AsyncTaskRunner extends EventEmitter {

    public get nworkers() {
        return this.workers.length;
    }

    public get isCleared() {
        return this.countRunningWorkers() === 0 && this.tasks.length === 0;
    }

    public static prefix = 'worker-';

    private workers: AsyncTaskWorker[] = [];
    private tasks: TaskFunction[] = [];

    public constructor(options: AsyncTaskRunnerOptions) {
        super();

        const prefix = options.prefix || AsyncTaskRunner.prefix;
        const nworkers = options.nworkers;

        for (let i = 0; i < nworkers; i++) {
            const worker = new AsyncTaskWorker(`${prefix}${i}`);
            worker
                .on('ready', () => {
                    this.distribute();
                })
                .once('dead', (err: Error, inst: AsyncTaskWorker) => {
                    inst.removeAllListeners();
                    if (this.listenerCount('worker-dead') > 0) {
                        this.emit('worker-dead', err, inst);
                    } else {
                        throw err;
                    }
                });
            this.workers.push(worker);
        }
    }

    public countReadyWorkers() {
        return this.workers.filter((worker) => worker.isReady).length;
    }

    public countRunningWorkers() {
        return this.workers.filter((worker) => worker.isRunning).length;
    }

    public addTasks(tasks: TaskFunction[]) {
        for (const task of tasks) {
            this.addTask(task);
        }
    }

    public addTask(task: TaskFunction) {
        this.tasks.push(task);
        this.distribute();
    }

    public async wait() {
        return new Promise((r) => {
            if (this.isCleared) {
                r();
            } else {
                this.once('cleared', r);
            }
        });
    }

    private distribute() {
        if (this.isCleared) {
            this.emit('cleared');
            return;
        }

        const worker = this.workers.find((w) => w.isReady);
        if (worker) {
            const task = this.tasks.shift();
            if (task) {
                worker.run(task);
            }
        }
    }
}

export interface AsyncTaskRunner {
    on(event: 'cleared', listener: () => any): this;
    on(event: 'worker-dead', listener: (error: Error, worker: AsyncTaskWorker) => any): this;
    once(event: 'cleared', listener: () => any): this;
    once(event: 'worker-dead', listener: (error: Error, worker: AsyncTaskWorker) => any): this;
}

export default AsyncTaskRunner;
